package com.yanghui.dtracing.reporter.zipkin;

import cn.hutool.core.util.StrUtil;
import com.yanghui.dtracing.core.opentracing.DTracerSpan;
import com.yanghui.dtracing.core.opentracing.DTracerSpanContext;
import com.yanghui.dtracing.core.opentracing.log.LogData;
import com.yanghui.dtracing.core.utils.NetUtils;
import io.opentracing.tag.Tags;
import zipkin2.Endpoint;
import zipkin2.Span;

import java.net.InetAddress;
import java.util.Arrays;
import java.util.Map;

public class ZipkinV2SpanAdapter {

    private InetAddress inetAddress;

    public Span convertToZipkinSpan(DTracerSpan span) {
        Span.Builder zipkinSpanBuilder = Span.newBuilder();
        DTracerSpanContext dtracerSpanContext = span.context();
        //traceId spanId parentId
        zipkinSpanBuilder.traceId(dtracerSpanContext.toTraceId());
        zipkinSpanBuilder.id(spanIdToLong(dtracerSpanContext.toSpanId()));
        if (!"-1".equals(dtracerSpanContext.getParentId())) {
            zipkinSpanBuilder.parentId(spanIdToLong(dtracerSpanContext.getParentId()));
        }
        // timestamp & duration zipkin是微妙,需要转换
        long start = span.getStartTime() * 1000;
        long finish = span.getEndTime() * 1000;
        zipkinSpanBuilder.timestamp(start);
        zipkinSpanBuilder.duration(finish - start);

        // kind
        Map<String, String> tagsWithStr = span.getStrTags();
        String kindStr = tagsWithStr.get(Tags.SPAN_KIND.getKey());
        if(!StrUtil.isEmpty(kindStr)){
            if (kindStr.equals(Tags.SPAN_KIND_SERVER)) {
                zipkinSpanBuilder.kind(Span.Kind.SERVER);
            } else {
                zipkinSpanBuilder.kind(Span.Kind.CLIENT);
            }
        }

        // Endpoint
        Endpoint endpoint = getZipkinEndpoint(span);
        zipkinSpanBuilder.localEndpoint(endpoint);

        // Tags
        this.addZipkinTags(zipkinSpanBuilder, span);

        // span name
        String operationName = span.getOperationName();
        String className = span.getClassName();
        if (StrUtil.isNotBlank(operationName) && StrUtil.isNotBlank(className)) {
            zipkinSpanBuilder.name(shorten(className + "." + operationName));
        } else {
            zipkinSpanBuilder.name("");
        }
        // Annotations
        this.addZipkinAnnotations(zipkinSpanBuilder, span);
        Span span1 = zipkinSpanBuilder.build();
        return span1;
    }

    private String shorten(String name){
        if(StrUtil.isEmpty(name) || name.length() <= 80){
            return name;
        }
        String[] ss = name.split("\\.");
        StringBuilder sb = new StringBuilder();
        for(int i=0;i<ss.length;i++){
            if(i >= ss.length - 2){
                sb.append(ss[i]).append(".");
                continue;
            }
            if(ss[0].length() >=2){
                sb.append(ss[i].substring(0,1)).append(".");
            }else {
                sb.append(ss[i]).append(".");
            }
        }
        return sb.deleteCharAt(sb.length() - 1).toString();
    }

    private void addZipkinAnnotations(Span.Builder zipkinSpan, DTracerSpan span) {
        for (LogData logData : span.getLogDataList()) {
            Map<String, ?> fields = logData.getFields();
            if (fields == null || fields.size() <= 0) {
                continue;
            }
            for (Map.Entry<String, ?> entry : fields.entrySet()) {
                // zipkin has been support default log event depend on span kind & serviceName
                if (!(entry.getValue().toString().equals(LogData.CLIENT_RECV_EVENT_VALUE)
                        || entry.getValue().toString().equals(LogData.CLIENT_SEND_EVENT_VALUE)
                        || entry.getValue().toString().equals(LogData.SERVER_RECV_EVENT_VALUE)
                        || entry.getValue().toString().equals(LogData.SERVER_SEND_EVENT_VALUE))) {
                    zipkinSpan.addAnnotation(logData.getTime() * 1000, entry.getValue().toString());
                }
            }
        }
    }

    private void addZipkinTags(Span.Builder zipkinSpan, DTracerSpan span) {
        for (Map.Entry<String, String> e : span.getStrTags().entrySet()) {
            zipkinSpan.putTag(e.getKey(), e.getValue());
        }
        for (Map.Entry<String, Number> e : span.getNumTags().entrySet()) {
            zipkinSpan.putTag(e.getKey(), e.getValue().toString());
        }
        for (Map.Entry<String, Boolean> e : span.getBolTags().entrySet()) {
            zipkinSpan.putTag(e.getKey(), e.getValue().toString());
        }
        zipkinSpan.putTag("costTime",String.valueOf(span.getCostTime()));
        addZipkinTagsWithBaggage(zipkinSpan, span);
    }

    private void addZipkinTagsWithBaggage(Span.Builder zipkinSpan, DTracerSpan span) {
        DTracerSpanContext context = span.context();
        if (context != null) {
            Map<String, String> sysBaggage = context.getBaggageItems();
            for (Map.Entry<String, String> e : sysBaggage.entrySet()) {
                zipkinSpan.putTag(e.getKey(), e.getValue());
            }
        }
    }

    private Endpoint getZipkinEndpoint(DTracerSpan span) {
        if (inetAddress == null) {
            inetAddress = NetUtils.getLocalAddress();
        }
        String appName = span.getServiceName();
        return Endpoint.newBuilder().serviceName(appName).ip(inetAddress).build();
    }

    public static long spanIdToLong(String spanId) {
        return FNV64HashCode(spanId);
    }

    /**
     * hash FNVHash64 查看 http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-param
     * @param data
     * @return
     */
    public static long FNV64HashCode(String data) {
        long hash = 0xcbf29ce484222325L;
        for (int i = 0; i < data.length(); ++i) {
            char c = data.charAt(i);
            hash ^= c;
            hash *= 0x100000001b3L;
        }
        return hash;
    }
}
